Chainer チュートリアル 〜ディープラーニング入門〜
Chainer チュートリアル 〜ディープラーニング入門〜
13. ニューラルネットワークの基礎
13.1. 事前に読んでおく必要がある章
2 章 Python 入門
4 章 微分の基礎
5 章 線形代数の基礎
6 章 確率・統計の基礎
7 章 単回帰分析と重回帰分析
8 章 NumPy 入門
あ〜、書いてあったのね、読んでおくべきところ。まあいいや勉強になったし。
13.2. ニューラルネットワークとは
ニューラルネットワークは、微分可能な変換を繋げて作られた計算グラフ (computational graph) です。
微分可能な変換をつなげる?
"Chainer" の名付け由来にもこの思想?表現?が関係ありそう
まずは下の図のような、円で表されたノード (node) に値が入っていて、ノードとノードがエッジ (edge) で繋がれているようなものを考えます。
https://tutorials.chainer.org/ja/_images/13_01.png
よく見る形式の図だね
この図でいうノードの縦方向の集まりのことを層 (layer) と呼びます。
そしてディープラーニング (deep learning) とは、層の数が非常に多いニューラルネットワークを用いた機械学習の手法や、その周辺の研究領域のことを指します。
はい
上の図は、ニューラルネットワークを用いて、ワインに関するいくつかの情報から、そのワインが「白ワイン」なのか「赤ワイン」なのか、というカテゴリを予測する分類問題を解く例を表しています。
分類問題
最初の層を入力層 (input layer)、最後の層を出力層 (output layer)といい、その間にある層は中間層 (intermediate layer) もしくは隠れ層 (hidden layer) といいます。
合計 3 層の構造(アーキテクチャ; architecture)となっています。
入力層
ノード数は 入力変数の数 + バイアス <- 変数$ x_0 = 1, 重み$ w_0 として重み行列を扱う
出力層
ノード数は
分類問題の場合は、分類したいカテゴリの数
連続的な実数値を予測する回帰問題の場合は、目標値の種類に合わせて決定する
自分で決定するパラメータを、ハイパーパラメータと呼びます。
中間層のノードをいくつにするか
中間層をいくつ重ねるか
ニューラルネットワークには、
上図のような層間のノードが互いに密に結合した全結合型 (fully-connected) のものだけでなく、
画像処理などでよく用いられる畳み込み型 (convolutional) のもの、
系列データの扱いによく用いられる再帰型 (recurrent) のものなど、いくつもの種類があります。
ふむふむ
別々の層間には別々の種類の結合の仕方を用いることができます。
つまり、1 つのニューラルネットワークの中に複数の種類の結合が混ざって現れることもあります。
なるほどね
例えば、入力に近いところでは畳み込み型の結合を用い、出力に近いところでは全結合型の結合を用いる、といったことがよく行われています。
ふむ
本章では、まず最も基本となる全結合型のみに着目して、解説を行います。 その他の種類については、後の章で解説を行います。
おっす
13.3. ニューラルネットワークの計算
13.3.1. 基本的な計算の流れ
それでは、「ワインの分類」を題材に、ニューラルネットワークにある入力が与えられたとき、どのように計算結果が得られ、その結果をどう使えば分類が行えるのかを、まずは数式を用いず、以下の図を見ながら理解しましょう。
https://tutorials.chainer.org/ja/_images/13_02.png
よっしゃ
分類問題では、出力された数値のうちどのノードが最も大きな値を持っているかによって、ニューラルネットワークの予測が決まります。 今回は、2 つの出力のうち y2=0.85 が最も大きな値となっているため、y2 に対応する「赤ワイン」が予測されたカテゴリとなります。
ふむふむ
このように、入力が与えられたとき、ニューラルネットワークの各層を順番に計算していき、出力まで計算を行うことを、順伝播 (forward propagation) と言います。
よく forward メソッドとして定義されるやつだね
ここからはこのような計算がニューラルネットワークの内部でどのように行われているのか、詳しく見ていきましょう。
ポイントは、ニューラルネットワークの各層では、前の層の出力値に「線形変換」と「非線形変換」を順番に施している、というところです。
それでは、線形変換とは何か、非線形変換とは何か、を順番に説明します。
お願いします
13.3.2. 線形変換
線形変換(注釈1)とは、入力ベクトルを $ h_0 としたとき(注釈2)、重み行列 $ W_{10} と、バイアスベクトル $ b_1 の 2 つを使って、次のような変換を行うことを言います。
$ {\bf u}_1 = {\bf W}_{10}{\bf h}_0 + {\bf b}_1
ふむ。回帰分析の時と同じような感じかな
以下の 2 層の全結合型ニューラルネットワークの図を見てみましょう。
https://tutorials.chainer.org/ja/_images/13_03.png
入力層のノードが持つ値は、結合重みと掛け合わされ、出力層のノードに伝わります。出力層の 1 つのノードには、複数のノードから計算結果が伝わってくるので、これらを全部足し合わせます。
具体的には、以下のような計算をします。
$ u_{11} = w_{11} h_{01} + w_{12} h_{02} + w_{13} h_{03} + b_{1}
$ u_{12} = w_{21} h_{01} + w_{22} h_{02} + w_{23} h_{03} + b_{2}
$ {\bf h}_{0} = \begin{bmatrix} h_{01} \\ h_{02} \\ h_{03} \end{bmatrix}, {\bf W}_{10} = \begin{bmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \end{bmatrix} , {\bf b}_1 = \begin{bmatrix} b_{1} \\ b_{2} \end{bmatrix} , {\bf u}_1 = \begin{bmatrix} u_{11} \\ u_{12} \end{bmatrix}
と定義すれば、
$ {\bf u}_1 = {\bf W}_{10}{\bf h}_0 + {\bf b}_1
と書くことができ、本節の冒頭に示した式と一致していることが分かります。
そうだね
$ {\bf h}_{0}の0はなんだろ?
0番目の入力データ、という意味のはず 0層目の入力データ、という意味っぽい。
$ {\bf W}_{10}の1と0は?
0層目の各ノードの値を1層目の各ノードの値に変換するための重み、という意味かな?
以上から、全結合型ニューラルネットワークでは、層と層の間で線形変換が行われていることが分かりました。
はい
ここで、出力層のノードを改めて見てみましょう。 ノードは真ん中に線が引かれて左右に分割されており、先程計算した出力値 u11,u12 はその左側の半円の中に書かれています。
https://tutorials.chainer.org/ja/_images/13_before_activation.png
そうだね、気になるね
ニューラルネットワークの隠れ層では、一つ前の層に線形変換を適用した結果を受け取り、そこへさらに非線形変換を適用したものを出力します。 その結果を、この右側の半円の中に書き入れます。
なんで?
次に、その非線形変換とは何なのか、またなぜ必要なのか説明していきます。
お願いします
13.3.3. 非線形変換
前節で紹介した線形変換のみでは、
下図左のような入出力間が線形な関係性はよく近似することができるとしても、
下図右のような入出力間が非線形な関係になっている場合には、観測データをうまく近似することができません。
https://tutorials.chainer.org/ja/_images/13_04.png
なるほどね
このようなケースに対応できるよう、ニューラルネットワークでは各層において、線形変換に続いて非線形変換を施し、層を積み重ねて作られるニューラルネットワーク全体としても非線形性を持つことができるようにしています。
ふーん。
ニューラルネットワーク全体としても非線形性を持つ どういうこと?
そして、この非線形変換を行う関数のことを、ニューラルネットワークの文脈では活性化関数 (activation function) と呼びます。
線形変換の節で用いた例に戻って、説明を進めます。
線形変換を行った結果 $ u_{11}, u_{12} のそれぞれに活性化関数 $ a を用いて非線形変換を行い、その結果を $ h_{11}, h_{12} とおきます。つまり、
$ h_{11} = a(u_{11}) \\ h_{12} = a(u_{12})
です。
お?これが$ {\bf h}_{1}か。この 1 は 1層目、ということっぽいな。
つまりn層目の入力ベクトルを$ {\bf h}_nと表現しているっぽい
この入力データを何万パターンも用意することになると思うので、その時はどういう表現になる?
例えば k パターン目の入力データの n 層目の入力ベクトルを$ {\bf x}_{kn}とする、とかかな?
これらは活性値(activation)と呼ばれ、もしもう一つ層が続いているならば、次の層に渡される入力の値となります。
活性化関数の具体例としては、下図に示す ロジスティックシグモイド関数 (logistic sigmoid function) が従来、よく用いられてきました。
https://tutorials.chainer.org/ja/_images/13_06.png
ヘェ〜。知ってる気がするけど忘れた。
しかし近年、層の数が多いニューラルネットワークではシグモイド関数は活性化関数としてほとんど用いられません。
ほー、なんで?
その理由の一つは、シグモイド関数を活性化関数に採用すると、勾配消失 (vanishing gradient) という現象によって学習が進行しなくなる問題が発生しやすくなるためです。 この問題の詳細は本章の最後で紹介します。
へぇ〜、そうなんだ
この問題を回避するために、最近では正規化線形関数 (ReLU: rectified linear unit) がよく用いられています。 これは、以下のような関数です。
https://tutorials.chainer.org/ja/_images/13_07.png
Chainer::Functions::Activation::Relu だ!
「Chainerの関数の1つで、活性化関数として使われるReLUだよ」というのが伝わってくるモデル分割がされているな^o^
クラス内に書かれてるdocumentにあるサンプルを試してみた
code:rb
1 pry(main)> require 'chainer' 2 pry(main)> x = Numo::SFloat-1, 0], 2, -3, [-2, 1 => Numo::SFloat#shape=3,2 3 pry(main)> (x < 0).any? => true
4 pry(main)> F = Chainer::Functions::Activation::Relu 5 pry(main)> y = F.relu(x) => Numo::SFloat#shape=3,2 負の要素が 0 に変換されてるだけだね
すなわち、ReLU は入力が負の値の場合には出力は 0 で一定であり、正の値の場合は入力をそのまま出力するという関数です。
えらいシンプルな関数どすなぁ〜
シグモイド関数では、入力が 0 から離れた値をとると、どんどん曲線の傾き(勾配)が小さくなっていき、平らになっていくことが一つ前の図からも見て取れます。 それに対し、ReLU 関数は入力の値が正であれば、いくら大きくなっていっても、傾き(勾配)は一定です。
それは関数を見ればわかるよ
これが後ほど紹介する勾配消失という問題に有効に働きます。
わからん、後ほどね
13.3.4. 数値を見ながら計算の流れを確認
https://tutorials.chainer.org/ja/_images/13_08.png
それでは、上の図のような回帰問題を解く 3 層の全結合型ニューラルネットワークを考えて、入力が与えられてから出力が得られるまでの一連の計算を、実際に数値を手計算しながら確認してみましょう。
はい
読んだ。なるほどそうだよね、という感じだ。
ここでは、ニューラルネットワークを用いて回帰問題を解く場合について説明を行うため、最終出力層に活性化関数を用いていません
回帰問題を解く場合 でない場合は最終出力層に活性化関数を用いるケースがあるということかな
注釈を読んだ
分類問題を解きたい場合は、クラス数と同じだけのノードを出力層に用意しておき、各ノードがあるクラスに入力が属する確率を表すようにします。 このため、全出力ノードの値の合計が 1 になるよう正規化します。
ふむ、そうだよね
これには、要素ごとに適用される活性化関数ではなく、層ごとに活性値を計算する別の関数を用いる必要があります。
そうだよね。(層だけに)
そのような目的に使用される代表的な関数には、ソフトマックス関数があります。
ここまでで、ニューラルネットワークの順伝播計算の中身について詳しく見てきました。
はい
このような計算過程は推論 (inference) とも呼ばれ、一般的には訓練が完了したモデルを用いて新しいデータに対して予測を行うことを指します。
そうだね
次節では、この $ {\bf W}_{10} や $ {\bf W}_{21} といったパラメータを決定するニューラルネットワークの訓練について解説します。
お願いします
13.4. ニューラルネットワークの訓練
重回帰分析では、目的関数をモデルのパラメータで微分して出てきた式を $ =0 とおいて、実際の数値を使うことなく変数のまま($ \bf x や $ \bf t と置いたまま)、解(最適なパラメータ)を求めることができました。 このように、変数のままで解を求めることを解析的に解くと言い、その答えのことを解析解 (analytical solution) と呼びます。
しかし、ニューラルネットワークで表現されるような複雑な関数の場合、最適解を解析的に解くことはほとんどの場合困難です。そのため、別の方法を考える必要があります。
なるほど別の手段
解析的に解く方法に対し、計算機を使って繰り返し数値計算を行って解を求めることを数値的に解くといい、求まった解は数値解 (numerical solution) と呼ばれます。
これから紹介する代表的な数値的解法では、求めたいパラメータの初期値 (initial value) を事前に決めておく必要があります。 なぜなら、この初期値から少しずつ値を更新していき、望ましい解に近づけていくためです。
このような手法は、最適化手法 (optimization method) と呼ばれるものの一種です。
1. モデルを設計し
2. 目的関数を定め
3. 目的関数を最適化するパラメータを求める
今回は、モデルとしては前節で説明に用いた 3 層のニューラルネットワークを用います。 このため、ステップ 1 はすでに完了しています。 次節からは、ステップ 2 以降を説明していきます。
はい、やっていきましょう。
13.4.1. 目的関数
ニューラルネットワークの訓練には、微分可能でさえあれば解きたいタスクに合わせて様々な目的関数を利用することができます。 ここでは、
回帰問題でよく用いられる平均二乗誤差 (mean squared error)
分類問題でよく用いられる交差エントロピー (cross entropy)
という代表的な 2 つの目的関数を紹介します。
平均二乗誤差 (mean squared error) は、回帰問題を解きたい場合によく用いられる目的関数です。 重回帰分析の解説中に紹介した二乗和誤差と似ていますが、各データ点における誤差の総和をとるだけでなく、それをデータ数で割って、誤差の平均値を計算している点が異なります。式で表すと、以下のようになります。
$ L = \frac{1}{N} \sum_{n=1}^N (t_n - y_n)^2
はい
今、ある入力 x を与えたときのこのニューラルネットワークの出力 y は、33 でした。ここで、もしこの入力に対応する目標値が t=20 だとすると、今全体のサンプルサイズが 1 だとして、平均二乗誤差は
$ L = \frac{1}{1} (20 - 33)^2 = 169
と計算できます。
そうだね
交差エントロピー (cross entropy) は、分類問題を解きたい際によく用いられる目的関数です。
例として、K クラスの分類問題を考えてみましょう。
ある入力 x が与えられたとき、ニューラルネットワークの出力層に K 個のノードがあり、それぞれがこの入力が k 番目のクラスに属する確率 $ y_k = p(y=k|x)を表しているとします。
ここで、$ x が所属するクラスの正解が、
$ {\bf t} = \begin{bmatrix} t_1 & t_2 & \dots & t_K \end{bmatrix}^{\rm T}
というベクトルで与えられているとします。
ほう、ベクトルでね。所属するのはどれか1つだけだよね?
ただし、このベクトルは $ t_k \ (k=1, 2, \dots, K) のいずれか 1 つだけが 1 であり、それ以外は 0 であるようなベクトルであるとします。
ですよね
これをワンホットベクトル(1-hot vector)と呼びます。
辞書に並んでる各単語のidとその単語を出現したかどうかを0,1で表すワンホットベクトルを使ってるんだと思う
以上を用いて、交差エントロピーは以下のように定義されます。
$ - \sum_{k=1}^{K}t_{k}\log y_{k}
これは、$ t_k が $ k = 1, \dots, K のうち正解クラスである一つの $ k の値でだけ $ 1 となるので、正解クラスであるような $ k での $ \log y_k を取り出して $ -1 を掛けているのと同じです
なるほど、これが入力$ xに対する交差エントロピーね
また、$ N 個すべてのサンプルを考慮すると、交差エントロピーは、
$ L = - \sum_{n=1}^{N} \sum_{k=1}^{K}t_{n, k}\log y_{n, k}
となります
ほい
ここまででステップ2の 目的関数を定め が終わったね
13.4.2. 目的関数の最適化
目的関数の値を最小にするようなパラメータの値を求めることで、ニューラルネットワークを訓練します。
本節では、最適化アルゴリズムの一つである勾配降下法 (gradient descent) を紹介します。
図中の点線は、パラメータ w を変化させた際の目的関数 L の値を表しています。 この図では簡単のために二次関数で表します。実際のニューラルネットワークの目的関数は、多次元で、かつもっと複雑な形をしていることがほとんどです。 さて、この目的関数が最小値を与えるような w は、どのようにして求められるのでしょうか。
https://tutorials.chainer.org/ja/_images/13_10.png
勾配とは、$ w を増加させた際に $ L が増加する方向を意味しています。今は $ L の値を小さくしたいわけですから、この傾きの逆方向へ $ w を変化させる、すなわち $ w から $ \frac{\partial L}{\partial w} を引けば良い ことになります。
ん?乱暴では?
あ、符号だけ見てる?っぽいな。勾配が正ならwを負の方向へ、勾配が負ならwを正の方向へ、ということかな
このときの w の一度の更新量の幅を調整するために、勾配に学習率 (learning rate) と呼ばれる値を乗じるのが一般的です。
ふむ
学習率を $ \eta とおくと、 $ w から $ \eta \frac{\partial L}{\partial w} を引くことで、勾配そのものの値を $ \eta にした量だけ $ w を更新することになります。更新後の $ w は、
$ w \leftarrow w - \eta \dfrac{\partial L}{\partial w}
となります。
なるほどね
ここで、 $ \leftarrow は、右側の値で左側の値を置き換える、つまり更新することを意味しています。
はい
こうして、学習率 × 勾配を更新量としてパラメータを変化させていくと、パラメータ w を求めたい L を最小にする w に徐々に近づけていくことができます。
そうだね
このような勾配を用いた目的関数の最適化手法を勾配降下法 (gradient descent) と呼びます。
はい
ニューラルネットワークは、基本的に微分可能な関数のみをつなげ合わせてできている(注釈4)ため、ニューラルネットワーク全体が表す関数も微分可能であり、訓練データセットを用いて勾配降下法によって目的関数を小さくする(局所)最適なパラメータを求めることができます。
基本的に微分可能 例外がある?
厳密には目的関数に微分不可能な点が存在する可能性はあります。
ほう。例えば?
例えば ReLU は x=0 で微分不可能なため、ReLU を含んだニューラルネットワークには微分不可能な点が存在することになります。このような場合、劣微分(subderivative, subdifferential)という考え方を導入し、全ての点で勾配が決定できるようにすることなどが行われます。
微分不可能であっても勾配が決定できれば良いわけだね
よし、これでステップ3 目的関数を最適化するパラメータを求める まで終わったのかな?
13.4.3. ミニバッチ学習
通常ニューラルネットワークを勾配降下法で最適化する場合は、データを一つ一つ用いてパラメータを更新するのではなく、いくつかのデータをまとめて入力し、それぞれの勾配を計算したあと、その勾配の平均値を用いてパラメータの更新を行う方法がよく行われます。
なるほどね、1つ1つやってたらキリがないもんね。
これをミニバッチ学習と呼びます。
https://tutorials.chainer.org/ja/_images/13_14.png
はい
100レコードずつでパラメータを更新してるんだね〜
ミニバッチ学習では、以下の手順で訓練を行います。
1. 訓練データセットから一様ランダムに $ N_{b} \ (>0) 個のデータを抽出する
2. その $ N_{b} 個のデータをまとめてニューラルネットワークに入力し、それぞれのデータに対する目的関数の値を計算する
3. $ N_b 個の目的関数の値の平均をとる
4. この平均の値に対する各パラメータの勾配を求める
5. 求めた勾配を使ってパラメータを更新する
平均値を使うんだね
そしてこれを、異なる $ N_b 個のデータの組み合わせに対して繰り返し行います。
はい
ミニバッチ学習と異なり、データセット全体を一度に用いて一回の更新を行う、すなわちグループへ分割しない方法をバッチ学習と呼びます。
勾配降下法では、データを用いてパラメータを 1 度更新するまでのことを 1 イテレーション (iteration) と呼び、
バッチ学習では訓練データセットのデータ全体を 1 度の更新に一気に用いるため、1 エポック = 1 イテレーション となります。
急にエポック出てきましたけど。データ全体を使うまでの期間?のことかな。 一方、例えばデータセットを 10 個のグループに分割してグループごとに更新を行うミニバッチ学習の場合は、1 エポック = 10 イテレーション となります。
はい
このようなミニバッチ学習を用いた勾配降下法は特に、確率的勾配降下法(stocastic gradient descent; SGD)と呼ばれます。
現在多くのニューラルネットワークの最適化手法はこの SGD をベースとした手法となっています。
へぇ〜
よし、今度こそステップ3 目的関数を最適化するパラメータを求める まで終わったかな
13.4.4. パラメータ更新量の算出
それでは今、下図のような3層の全結合型ニューラルネットワークを考え、3 次元の入力ベクトルから、1 次元の値を出力し、正解の値を予測する回帰問題を題材に、パラメータを更新する式の導入を行ってみましょう。
https://tutorials.chainer.org/ja/_images/13_16.png
あ、はい
そっか、ステップ3 目的関数を最適化するパラメータを求める は結局、パラメータをいい感じに更新してく業、なので、更新の仕方をちゃんと理解しておく必要があるよね
それではまず、出力層に近い方のパラメータ、$ {\bf w}_2 についての $ L の勾配を求めてみましょう。
(色々定式化しているのは省いた)
NumPyで計算してみる業
簡単に計算できるなぁ
出力層に近い方の その方が計算しやすいのかな
$ {\bf w}_2 \leftarrow {\bf w}_2 - \eta \frac{\partial L}{\partial {\bf w}_2}の$ \frac{\partial L}{\partial {\bf w}_2}を計算できた
$ \etaはどうする?
学習率が大きすぎると、繰り返しパラメータ更新を行っていく中で目的関数の値が振動したり、発散したりしてしまいます。 逆に小さすぎると、収束に時間がかかってしまいます。 そのため、この学習率を適切に決定することがニューラルネットワークの学習においては非常に重要となります。
ですよね
シンプルな画像認識のタスクなどでは大抵、0.1 から 0.01 程度の値が最初に試される場合が比較的多く見られます。
ふーん
次に、$ {\bf w}_1 の更新量も求めてみましょう。
そのためには、$ {\bf w}_1 で目的関数 $ L を偏微分した値が必要です。
ごにょごにょやると $ \frac{\partial L}{\partial {\bf w}_1}を計算できた
13.4.5. 誤差逆伝播法(バックプロパゲーション)
ここまでで、各パラメータについての目的関数の導関数を手計算により導出して実際に勾配の数値計算を行うということを体験しました。 では、もっと層数の多いニューラルネットワークの場合は、どうなるでしょうか。
つらそう
同様に手計算によって全てのパラメータの勾配を求めることも不可能ではないかもませんが、層数が多くなれば膨大な時間がかかるでしょう。
だよね
しかし、ニューラルネットワークの微分可能な関数を順に適用するという性質を用いると、コンピュータによって自動的に勾配を与える関数を導き出すことが可能です。
はい〜
まず、合成関数の偏微分は、連鎖律によって複数の偏微分の積の形に変形できることを思い出しましょう。
はい
下図は、
ここまでの説明で用いていた 3 層の全結合型ニューラルネットワークの出力を得るための計算(順伝播)と、
その値を使って目的関数の値を計算する過程を青い矢印で、
そして前節で手計算によって行った各パラメータによる目的関数の偏微分を計算する過程を赤い矢印で
表現した動画となっています。
https://tutorials.chainer.org/ja/_images/13_backpropagation.gif
おす
今、上図の青い矢印で表されるように、新しい入力 $ \bf x がニューラルネットワークに与えられ、それが順々に出力側に伝わっていき、最終的に目的関数の値 $ l まで計算が終わったとします。
ふむ
すると次は、目的関数の出力の値を小さくするような各パラメータの更新量を求めたいということになりますが、
そっすね、
このために必要な目的関数の勾配は、各パラメータの丸いノードより先の部分(出力側)にある関数の勾配だけで計算できることが分かります。
確かにその通りだ!
ニューラルネットワークを構成する関数が持つパラメータについての目的関数の勾配を、順伝播で通った経路を逆向きにたどるようにして途中の関数の勾配の掛け算によって求めるアルゴリズムを 誤差逆伝播法 (backpropagation) と呼びます。
なるほどね
13.4.6. 勾配消失
活性化関数について初めに触れた際、シグモイド関数には勾配消失という現象が起きやすくなるという問題があり、現在はあまり使われていないと説明をしました。 その理由についてもう少し詳しく見ていきましょう。
シグモイド関数の導関数を思い出してみます。
$ a'\left( u\right) = a\left( u\right) \left( 1-a\left( u\right) \right)
https://tutorials.chainer.org/ja/_images/13_17.png
中央下の導関数の形から明らかなように、入力が原点から遠くなるにつれ勾配の値がどんどん小さくなり、0に漸近していくことが分かります。
ふむ、明らかだね
各パラメータの更新量を求めるには、前節で説明したように、そのパラメータよりも先のすべての関数の勾配を掛け合わせる必要がありました。
うむ
つまり、シグモイド関数がニューラルネットワーク中に現れるたびに、目的関数の勾配は、多くとも 0.25 倍されてしまいます。
ですね
層数が増えていけばいくほど ...(略)... 入力に近い層に流れていく勾配はどんどん 0 に近づいていってしまいます。
ふむふむ
活性化関数としてシグモイド関数を使用した場合、目的関数の勾配が入力に近い関数が持つパラメータへほぼ全く伝わらなくなってしまいます。
入力層に近い関数が持つパラメータは、更新の量が小さくなる、ということ
どんなに目的関数が大きな値になっていても、入力層に近い関数が持つパラメータは変化しなくなります。
つまり初期化時からほとんど値が変わらなくなるということになり、学習が行われていないという状態になるわけです。
意味なくなってしまう
これを勾配消失と呼び、長らく深い(十数層を超える)ニューラルネットワークの学習が困難であった一つの要因でした。
なるほどね
この解決策として、ReLU 関数が提案され、多層のニューラルネットワークに対する学習も勾配消失を回避しながら行うことができるようになっています。
14. Chainer の基礎
本章では、ディープラーニングフレームワーク Chainer の基本的な機能を使って、Chainer を使った訓練がどのような処理で構成されているかを簡潔に紹介します。
やっとだ!
本章では、 scikit-learn に用意されている Iris データセットを利用し、アヤメの品種を分類する問題に取り組みます。 個々のステップについて以降で詳しく説明していきます。
データセットを「訓練データセット」「検証データセット」「テストデータセット」の 3 つに分割します。
あら、3つに分割?検証データとテストデータは別なのか?
まず全体を訓練データセットと検証データセットを合わせたものとテストデータセットの 2 つに分割します。
まずは2つに分割。 訓練データセットと検証データセットを合わせたもの と テストデータセット の2つ。
次に訓練データセットと検証データセットに分割します。
訓練データセットと検証データセットを合わせたものをさらに2つに分ける、ということか。
つまり
訓練データセットと検証データセットを合わせたもの (70%)
訓練データセット(49%)
検証データセット(21%)
テストデータセット (30%)
本章以降では、ネットワークを構成する「層 (layer) 」という言葉を、これまでの説明で用いてきたノードの集まりに対してではなく、訓練可能なパラメータを持つ関数に対して用います。
なるほど?ここで言葉の定義を切り替えてくるのね
まあでも了解です
「層」が何を指すかは文献によって異なっている場合もあるため、注意してください。
はい
Chainer はニューラルネットワークの層を構成するための微分可能な関数を多く提供しています。 それらの関数は以下の 2 つに大別されます。
パラメータを持つ関数 (層):リンク
パラメータを持たない関数:ファンクション
ほう、なるほど
リンクは、chainer.links モジュール以下に、ファンクションは、chainer.functions モジュール以下に定義されています。
おす
Chainer ではネットワークを作る方法がいくつか用意されています。
便利
ここでは簡単にネットワークが作ることができる Sequential クラスを利用し、全結合層が 3 つ、活性化関数に ReLU 関数を利用したネットワーク net を作ります。Chainerでは全結合層は Linear クラスとして定義されています。
便便便利
Iris のデータは入力変数が 4 つですので、最初の全結合層の入力次元数が 4 になります。 また、Iris のクラス数は 3 なので、最後の全結合層の出力次元数は 3 です。
加えて 全結合層が 3 つ だから Linear クラスが3つ出てくるはずだね
code:py
from chainer import Sequential
# net としてインスタンス化
n_input = 4
n_hidden = 10
n_output = 3
net = Sequential(
L.Linear(n_input, n_hidden), F.relu,
L.Linear(n_hidden, n_hidden), F.relu,
L.Linear(n_hidden, n_output)
)
わかってきた感がある〜
今回は、分類タスクによく利用される目的関数の中から、交差エントロピーを採用することにします。
分類問題だもんね
注釈あった
交差エントロピーを計算するためには、 2 つの確率分布(正解ラベルと予測確率分布)を入力する必要があります。
ほうほう
しかしながら、ネットワークの出力値は確率分布にはなっていないため、出力値に対してソフトマックス関数を適用することで確率分布への正規化を行い、その後に交差エントロピーを計算するという手続きが必要となります。
なるほど
Chainer では、内部でソフトマックス関数を適用した後、交差エントロピーを計算する sofmax_cross_entropy() が定義されています。今回はこれを目的関数として利用することで、明示的にソフトマックス関数を呼ばずに、ネットワークの出力値を直接 softmax_cross_entropy() に渡して交差エントロピーを計算できます。
便利かよ〜
Chainer::Functions::Loss::SoftmaxCrossEntropy
引数の渡し方とかも比べてみよう〜
今回は、最適化手法として、確率的勾配降下法 (SGD) を利用します。
パラメータを最適化するぞ〜
Chainer では chainer.optimizers に各最適化手法を実行するためのクラスが用意されており、確率的勾配降下法は SGD という名前で定義されています。
便利
code:py
optimizer = chainer.optimizers.SGD(lr=0.01) # 学習率=0.01
optimizer.setup(net) # オプティマイザーにネットワークをセットするんだね。
実際に訓練を実行します。訓練は以下の処理を繰り返します。
1. 訓練用のバッチを準備
2. 予測値を計算し、目的関数を適用 (順伝播)
3. 勾配を計算 (逆伝播)
4. パラメータを更新
この辺は地味に結構コード書くんだなぁ。for文を回したり。
iterator という概念が便利っぽいな
これに加えて、訓練がうまくいっているか判断するために、訓練データを利用した分類精度と検証データを利用した目的関数の値と分類精度を計算します。
検証データセット は 訓練がうまくいっているか判断するため のものなのかな
結局、うまく行ってるかどうかの判断もしにくいなぁ
訓練データセット だけで分類精度が高くなっていて 検証データセット で低かったら、過学習になってる可能性があるよ、とかかな? でもそれ テストデータセット でもわかりそうじゃない?
結局のところ、 訓練データセット 以外にデータセットをいくつか用意しておくと安心だよ、という意味以上のものがないような気がする。
テストデータセットA テストデータセットB という名前でもよくない?
chainer.using_config('train', False) は、対象のデータを使用した訓練を行わない場合に指定します。
一方、chainer.using_config('enable_backprop', False) は、計算グラフの構築を行わない場合に指定します。
検証データでは勾配を計算しないため、計算グラフは不要となります。これを指定することで計算グラフの構築処理が省かれるため、メモリ消費量を節約することができます。
こういうのも気にしないとね
最後に、訓練したネットワークを保存します。
そうそう、これできないとね
訓練済みのネットワークを用いてテストデータに対して推論を行います。
そうそう。これこれ。
gem作りたいからね。事前に訓練したネットワークを作って保存しておいて、それを使って推論できるようにしないと。
ネットワークを読み込むには、最初に訓練済みのネットワークと同様のクラスのインスタンスを作ります。
はい
このネットワークのインスタンスに訓練済みのネットワークのパラメータを読み込ませます。
はい
訓練済みのネットワークの準備ができたら、実際に推論を行います。
便利〜
本章では、Chainer を用いたモデルの訓練、及び推論の基本的な使い方を解説しました。
次章では、モデルの精度改善や訓練時間の短縮を行うための、より実践的な機能について紹介していきます。
よっしゃ
15. Chainer の応用
前章で説明した Chainer を用いてネットワークを訓練するまでに必要なステップは、以下の 5 つでした。
- Step 1:データセットの準備
- Step 2:ネットワークを決める
- Step 3:目的関数を決める
- Step 4:最適化手法を選択する
- Step 5:ネットワークを訓練する
本章では、これらの各ステップに対して Chainer の機能を活用した工夫を加え、結果を改善する方法を解説します。
結果を改善するのね
Step 1 : データセットの準備(応用編)
Chainer が提供するいくつかのデータセットを扱うためのクラスを用いて同様のことを行ってみます。
前章では分割する時 from sklearn.model_selection import train_test_split を使ったね
データ取得は from sklearn.datasets import load_iris を使う方法。前章と同様。
入力値を並べた配列と目標値を並べた配列を与えると、1 つ 1 つを取り出して対応するペアを作って返してくれる
便利
code:py
from chainer.datasets import TupleDataset
dataset = TupleDataset(x, t)
データセットを訓練用・検証用・テスト用に分割するのに便利な関数が Chainer にも用意されています。
from chainer.datasets import split_dataset_random
train, test = Chainer::Datasets::MNIST.get_mnist でやってる
前章では、訓練データセットの順番を毎エポックシャッフルする方法として np.random.permutation() 関数が用いられていましたが、Chainer ではこのようなネットワークの訓練に際してよく行われるデータセットへの操作を抽象化したイテレータ(iterator)が提供されています。
お、出た。iteratorだ。
イテレータは、データセットオブジェクトを与えると、順番のシャッフルやバッチサイズ個だけデータをまとめて返すなどの操作を自動的に行なってくれるものです。
便利だ
ここでは最もシンプルなイテレータである SerialIterator を紹介します。
複雑なイテレータもあるのか?
code:py
from chainer.iterators import SerialIterator
train_iter = SerialIterator(train, batch_size=4, repeat=True, shuffle=True)
minibatch = train_iter.next() # 最初のイテレート。バッチサイズが4なので4レコードずつ取得される。
repeat 引数には、next() を繰り返し実行してデータセット内の全てのデータを取り出し終えたあとに、次の next() の呼び出しに対してまたデータセットの先頭からデータを取り出して次のミニバッチを返すかどうかを指定します。 1 エポック以上訓練を行う場合は訓練データセット内のデータを複数回用いるので、これを True にします。
shuffle 引数には、データセット内のデータの順番をエポックごとに自動的にシャッフルするかどうかを指定します。
Step 2 : ネットワークを決める(応用編)
前章ではネットワークを Sequential クラスを用いて定義しました。 ここでは Chain クラスを継承してネットワークを定義する方法を説明します。
class MLP < Chainer::Chain
Chain クラスは Link クラスを継承しており、前章で解説したリンクと同様に扱うことができます。
Linkクラスは全結合層を表現する時に出てきたやつだね
内部に複数のリンクを保持しておくことができ、呼び出されたときに実行される巡伝播計算を forward() メソッドに記述しておくことで、複数のリンクやファンクションを組み合わせた独自の層を作るのに使うことができます。
なるほど、Chainクラス便利だ
また、ネットワークの部分構造を記述したり、それ自体で 1 つのネットワークの定義とすることもできます。
ふむふむ
それでは、前章で定義した L.Linear 層を 3 つ持つネットワークと同じものを Chain クラスを継承した Net というクラスを作って定義してみましょう。
code:py
import chainer
import chainer.links as L
import chainer.functions as F
class Net(chainer.Chain):
def __init__(self, n_in=4, n_hidden=3, n_out=3):
super().__init__()
with self.init_scope():
self.l1 = L.Linear(n_in, n_hidden)
self.l2 = L.Linear(n_hidden, n_hidden)
self.l3 = L.Linear(n_hidden, n_out)
def forward(self, x):
h = F.relu(self.l1(x))
h = F.relu(self.l2(h))
h = self.l3(h)
return h
net = Net()
かなりわかりやすいな〜
前章のこれもまあまあよかったけどね
code:py
from chainer import Sequential
# net としてインスタンス化
n_input = 4
n_hidden = 10
n_output = 3
net = Sequential(
L.Linear(n_input, n_hidden), F.relu,
L.Linear(n_hidden, n_hidden), F.relu,
L.Linear(n_hidden, n_output)
)
input/outputの関係が見えるからわかりやすく感じるんだろうなぁ
Sequentialを使う形だと、引数に与えられた順にinput/outputが流れていくような感じで、暗黙的になってる
__init__() メソッドでは、init_scope() によって作られるコンテキストの中で L.Linear 層 を 3 つ作成し、それぞれ別々の名前の属性に代入しています。
なんで init_scope() によって作られるコンテキストの中 で それぞれ別々の名前の属性に代入 しているのかというと?
このコンテキストの中で属性に代入されたリンクが持つパラメータは、オプティマイザによるパラメータ更新の対象として登録されます。
なるほど、パラメータが更新されないと困るもんね〜
逆にいうと...?
このコンテキストの外でリンクを作成し属性に代入を行っても、そのリンクが持つパラメータはオプティマイザによるパラメータ更新の対象にならないため、注意が必要です。
なるほどね
Step 3 : 目的関数を決める(応用編)
前章では、分類問題を解くために F.softmax_cross_entorpy という目的関数を使用しました。
そうだね
$ \hat{\bf y} = {\rm softmax}({\bf y})
$ L(\boldsymbol\Theta) = \sum_{k=1}^K \hat{y}_k \log t_k
ここで、この目的関数に正則化項 (regularization term) を追加してみましょう。
お?新顔だ。
正則化 (regularization) とは、過学習を防ぐために、目的関数に新たな項を追加して、モデルの複雑さに罰則を科したり、パラメータのノルムの大きさに罰則をかけたりすることを指します。
過学習を防ぐために、目的関数に新たな項を追加 お〜、結果の改善につながりそう!
てかここまでのStep1,2の改善は、結果の改善じゃなくてリファクタリングじゃね?まあいいけど。
ここでは、重み減衰 (weight decay) と呼ばれる正則化を適用してみましょう。
ほう、なんですか
これは、L2正則化 (L2 regularization) とも呼ばれます。
いきなり別名のご紹介。これは強そうな予感。
重み減衰を行う場合、最適化する目的関数は以下になります。
$ L(\boldsymbol\Theta) + \lambda \frac{1}{2} \sum_w || w ||^2
$ wはネットワークのパラメータ、とのこと (= バイアスベクトルは重み減衰の対象外だよ、ということ)
この項を目的関数に加えると、ネットワークの重みの絶対値が大きくなりすぎないようにする効果があり、過学習を防ぐために役立ちます。
逆にいうと、ネットワークの重みの絶対値が大きくなりすぎると過学習が発生しやすくなる、ということ? ネットワークの重みの絶対値が大きくなりすぎないようにする効果があり これ自体には納得感がある
目的関数を小さくするような方向にパラメータを更新していくから、目的関数への正の影響が大きいほどパラメータ自体は小さい値に更新されやすくなるよね
𝜆 は正則化の強さをコントロールします。
ふむふむ
Chainer は、パラメータを更新する際に更新計算をカスタマイズする方法を 2 種類提供しています。
ほう?
1 つは、ネットワークが持つパラメータ全てに対して一様に、更新時にある処理を行いたい場合に使える、オプティマイザフック(optimizer hook)という機能です。
ネットワークが持つパラメータ全て これは重み行列$ \bf Wとバイアスベクトル$ \bf bの両方のことかな?
これは、Optimizer オブジェクトの add_hook() メソッドに更新時に全パラメータに対して行いたい処理を記述した関数を渡して使用します。
ふむ
もう 1 つは、パラメータごとに別々に処理を行いたい場合に使える方法で、ネットワークのパラメータが持っている UpdateRule というオブジェクトにフック関数を追加します。
ほう?
全ての訓練可能なパラメータは、Optimizer オブジェクトの setup() メソッドに渡された際に update_rule という属性に UpdateRule オブジェクトがセットされます。
そうなんですね
この UpdateRule オブジェクトは、最適化手法によって異なる更新ルールが記述されたもので、add_hook() メソッドを持ち、ここにオプティマイザフックまたは任意の関数を追加することができます。
なるほどね
これを用いると、更新時にパラメータごとに別の関数を読んで更新計算をカスタマイズすることができます。
便利だね
重み減衰は、前述のようにバイアスには適用しないため、今回は UpdateRule に対してフック関数を追加します。
後者の方だね。update_rule属性のadd_hook()メソッドに重み減衰を表す関数を入れる、ということになるかな。
Chainer では、chainer.optimizer_hooks モジュール以下に数種類の正則化手法が定義されており、重み減衰は WeightDecay というクラスとして定義されています
便利だね〜
それでは、これを用いて前章と同様に最適化手法として SGD を採用しつつ、新たに重み減衰を適用するような オプティマイザを定義しましょう。
確率的勾配降下法(stocastic gradient descent; SGD) ね
code:py
from chainer import optimizers
from chainer.optimizer_hooks import WeightDecay
optimizer = optimizers.SGD(lr=0.001) # 学習率を 0.01 に設定
optimizer.setup(net)
for param in net.params():
if param.name != 'b': # バイアス以外だったら
param.update_rule.add_hook(WeightDecay(0.0001)) # 重み減衰を適用 (λ=0.0001)
ネットワークの持つパラメータは、Chain クラスの params() メソッドを使って取得することができます。
なるほどね。
てか (lr=0.001) # 学習率を 0.01 に設定 っておかしくない?typoかな?
Step 4 : 最適化手法を選択する(応用編)
前章では、Chainer が提供している最もシンプルな最適化手法の一つである SGD を用いていました。
確率的勾配降下法(stocastic gradient descent; SGD) ね
Chainer は SGD の他にも多くの最適化手法を提供しています。
ここでは、その中でも代表的な手法の一つである MomentumSGD という手法を用いるように変更を加えてみましょう。
hai
MomentumSGD は SGD の改良版で、パラメータ更新の際に前回の勾配を使って更新方向がスムーズになるように工夫するもので、更新式は以下になります。
前回の勾配 を使うと 更新方向がスムーズになる のか?
使い方次第だとは思うけど...
$ w \leftarrow w - \eta \Delta w_{t} + \mu \Delta w_{t - 1}
ん?前回の勾配 $ \Delta w_{t - 1}を足してるな?
$ \mu は前回の勾配に掛ける係数で、多くの場合 $ 0.9 程度が用いられます。
ほう?仮に学習率を0.01として、他の数字も当てはめて考えてみるか
$ w \leftarrow w - 0.01 \Delta w_{t} + 0.9 \Delta w_{t - 1}で、
$ \Delta w_{t} = 2, \Delta w_{t - 1}=4とかにすると
$ w \leftarrow w - 0.01*2 + 0.9*4 = w + 0.34となるね
わからんけど、前回の勾配の影響、大きすぎない?
これを前節で解説した重み減衰と合わせて用いると、更新式は以下のようになります。
$ w \leftarrow w - \eta \Delta w_{t} + \mu \Delta w_{t - 1} - \eta \lambda w
hai
それでは、MomentumSGD に加えて重み減衰を用いるオプティマイザを定義してみましょう。
MomentumSGDを使って引数に momentum=0.9 を渡すだけだね
code:py
from chainer import optimizers
from chainer.optimizer_hooks import WeightDecay
optimizer = optimizers.MomentumSGD(lr=0.001, momentum=0.9)
optimizer.setup(net)
for param in net.params():
if param.name != 'b': # バイアス以外だったら
param.update_rule.add_hook(WeightDecay(0.0001)) # 重み減衰を適用
Step 5 : ネットワークを訓練する(応用編)
最後に、ネットワークの訓練を高速化するために GPU を用いる方法を紹介します。
あ〜、結果の改善じゃなくて、速度の改善か〜
まあ速度の改善が訓練の改善になって、最終的に結果の改善につながるんだろうけど
より大きなデータセットを使い、より多くの層を持つニューラルネットワークを訓練しようとすると、CPU だけでは膨大な時間がかかってしまいます。 そこで、GPU を使って計算を高速化する必要が出てきます。
はい
GPU を利用したい場合、ネットワークの訓練を開始する前に気をつけることは主に以下の 2 つです。
- ネットワークを to_gpu() を用いて GPU メモリ上に転送しておく
- ネットワークに入力するデータを CuPy の ndarray に変換しておく
はい
それでは、訓練ループを GPU を使用する形に変更してみましょう。
長いのでコードは貼らない
for epoch in range(n_epoch): とか while True: とか書いてるの、端的に言ってダセェな
訓練に GPU を用いた場合は、save_npz() 関数を使ったネットワーク重みの保存の際に、まずネットワークのパラメータを CPU メモリ上に転送することを忘れないでください。
忘れそう
code:py
from chainer.serializers import save_npz
net.to_cpu() # ネットワークのパラメータを CPU メモリ上に転送
save_npz('net.npz', net)
16. トレーナとエクステンション
前章までは、訓練ループを Python の while 文を使って記述してきました。 訓練ループは、以下のような定型的な処理を繰り返し行うものでした。
訓練ループで行われること
1. イテレータがデータセットからデータを取り出し、ミニバッチを作成する
2. ミニバッチをネットワークに入力し、順伝播の計算を行う
3. ネットワークの出力と目標値を使って目的関数の値(損失)を計算する
4. 逆伝播によって各パラメータについての目的関数の勾配を計算する
5. 求まった勾配を使ってパラメータを更新する
このような定型的な処理を段階ごとに別々のオブジェクトにまとめ、さらにそれらのオブジェクトをまとめたものがトレーナ (trainer) です。
先ほどの発言
for epoch in range(n_epoch): とか while True: とか書いてるの、端的に言ってダセェな
がトレーナを使うことでスマートに解決されるのか〜
トレーナにはエクステンション (extension) が用意されており、訓練曲線の可視化、訓練の途中状態やログの保存など、訓練ループ中に付加的な処理を追加することが容易になっています。
なるほどなるほど
16.1. トレーナの使用方法
16.1.1. トレーナの概要
下図はトレーナを構成するオブジェクトの関係図です。
https://tutorials.chainer.org/ja/_images/14_01.png
なるほどわかりやすい
16.1.2. データセットの準備
ここでは scikit-learn の標準機能で用意されているデータセットのうち、Chainer の基礎の章でも利用した Iris というデータセットを使用します。
はい
これは、アヤメ科の植物のうち 3 種(Setosa、Versicolour、Virginica)のいずれかであるサンプル 150 個について、
花弁の長さ
花弁の幅
がく片(注釈1)の長さ
がく片の幅
の 4 つを測って集めたものです。
そうだね
ここでは、各サンプルを見てそれが 3 種類の植物のうちどれに属するのかを予測します。
分類問題っすね
scikit-learn の機能を使ってデータセットを読み込み、
code:py
from sklearn.datasets import load_iris
# Iris データセットの読み込み
dataset = load_iris()
# 入力値と目標値を別々の変数へ格納
x = dataset.data
t = dataset.target
# Chainer がデフォルトで用いる float32 型へ変換
x = np.array(x, np.float32)
t = np.array(t, np.int32)
Chainer の TupleDataset クラスを利用してデータセットオブジェクトを作成します。
code:py
from chainer.datasets import TupleDataset
# 入力値と目標値を引数に与え、TupleDataset オブジェクトを作成
dataset = TupleDataset(x, t)
ここで、データセット全体を 7 : 1 : 2 の比率で分割し、それぞれを訓練用、検証用、テスト用のデータセットとします。
code:py
from chainer.datasets import split_dataset_random
n_train = int(len(dataset) * 0.7)
n_valid = int(len(dataset) * 0.1)
train, valid_test = split_dataset_random(dataset, n_train, seed=0)
valid, test = split_dataset_random(valid_test, n_valid, seed=0)
print('Training dataset size:', len(train))
print('Validation dataset size:', len(valid))
print('Test dataset size:', len(test))
Iris のデータセット 150 件のうち、105 件が訓練用データセットとして取り出されました。 残りの 45 件のうち 15 件が検証用データセットに、30 件がテスト用データセットとなります。
はい
16.1.3. イテレータの準備
訓練用データと検証用データそれぞれに対してイテレータを作成します。
はい
16.1.4. ネットワークの準備
Chain を使って 3 層の多層パーセプトロン (multilayer perceptron、以後 MLP) を定義します。
code:py
class MLP(chainer.Chain):
def __init__(self, n_mid_units=100, n_out=3):
super().__init__()
with self.init_scope():
self.fc1 = L.Linear(None, n_mid_units)
self.fc2 = L.Linear(n_mid_units, n_mid_units)
self.fc3 = L.Linear(n_mid_units, n_out)
def forward(self, x):
h = F.relu(self.fc1(x))
h = F.relu(self.fc2(h))
h = self.fc3(h)
return h
変数名が違うとかはまあいいとして
まず__init__が違う
def __init__(self, n_in=4, n_hidden=3, n_out=3):
1層目が違う
self.l1 = L.Linear(n_in, n_hidden)
変数が4つ 花弁の長さ 花弁の幅 がく片(注釈1)の長さ がく片の幅 だから n_in=4 を渡すんだよね
L.Linear(None, n_mid_units) ... None ってなってるけどいいのかねぇ
他は同じか
ここで、self.fc1 に格納された L.Linear 層は、インスタンス化の際に第 1 引数に None をとっている点に注意してください。 これは、初めてデータがこのネットワークに渡された際に、自動的にこの層の入力側のノード数を決定するということを意味しています。
解説書いてあった〜〜
なるほどなぁ、便利だ。より一般化したのね。
code:rb
init_scope do
@l1 = L.new(nil, out_size: n_units)
@l2 = L.new(nil, out_size: n_units)
@l3 = L.new(nil, out_size: n_out)
end
うひょ〜、めっちゃ nil 使ってるやん。
@l2 @l3 は n_units 使えばいいのに!使わなくてもどうせ 自動的にこの層の入力側のノード数を決定する から要らないぜ、というスタンスなんだろうな。
16.1.5. アップデータの準備
訓練ループを自分で書く場合には、ループの各イテレーションにおいて行われる以下の 5 つのステップを明示的に記述する必要がありました。
1. データセットからミニバッチを作成
2. 順伝播(forward)の計算
3. 損失(loss)の計算
4. 逆伝播(backward)の計算
5. オプティマイザによってパラメータを更新
そうだね
アップデータを用いることで、これらの一連の処理を隠蔽し、簡潔に記述することができます。
👏
アップデータには、イテレータとオプティマイザを渡す必要があります。
イテレータはデータセットを持っており、上記のステップ 1. を行います。
オプティマイザはネットワークを持っており、上記のステップ 2. 〜 5. を行います。
んなるほど
それでは、イテレータはすでに準備したため、ネットワークとオプティマイザを定義し、アップデータオブジェクトを作成してみましょう。
code:py
from chainer import optimizers
from chainer import training
# ネットワークを作成
predictor = MLP()
# L.Classifier でラップし、損失の計算などをモデルに含める
net = L.Classifier(predictor)
# 最適化手法を選択してオプティマイザを作成し、最適化対象のネットワークを持たせる
optimizer = optimizers.MomentumSGD(lr=0.1).setup(net)
# アップデータにイテレータとオプティマイザを渡す
updater = training.StandardUpdater(train_iter, optimizer, device=-1) # device=-1でCPUでの計算実行を指定
L.Classifier 急に出てきたけど、きみ誰?
L.Classifier は、ネットワークへ渡される入力値 x に加えて、分類問題においては正解ラベルとなる目標値 t も引数にとり、指定された目的関数の計算を行って、損失を返すようネットワークをラップします。
ほ〜、便利そうじゃん
model = Chainer::Links::Model::Classifier.new(MLP.new(args[:unit], 10), lossfun)
Chainer::Links::Model::Classifier というクラスがあるんだね
デフォルトの目的関数はソフトマックス交差エントロピー(F.softmax_cross_entropy)に設定されています。
また、L.Classifier はインスタンス化を行う際にネットワークを引数にとり、これを predictor という属性に格納します。
つまり、上記コードにおける初めの predictor は、net.predictor という属性に格納されています。
predictor = MLP() という変数で置いてるのはそういう背景があったのね
最後の行で作成している StandardUpdater は、複数あるアップデータの実装のうち、最もシンプルなものです。
ほう〜
updater = Chainer::Training::StandardUpdater.new(train_iter, optimizer, device: device)
他にも、複数 GPU を用いてネットワークの訓練を行うための MultiprocessParallelUpdater などがあります。
大量のデータを複数ネットワークで一気に訓練したいときは便利そう
16.1.6. トレーナの作成と終了タイミングの指定
訓練を開始するために、トレーナを作成しましょう。
トレーナは、Trainer クラスをインスタンス化して作成します。
いよいよトレーナの登場だ〜
トレーナは、アップデータを用いて訓練のイテレーションを回します。
その繰り返しの終了タイミングは、Trainer のコンストラクタの第 2 引数 stop_trigger に (整数, 単位) というタプルを渡して指定します。
ふむふむ
単位 には 'iteration' もしくは 'epoch' のいずれかの文字列を指定します。
1 イテレーション (iteration) とはミニバッチ 1 個分を処理することを表し、1 エポック (epoch) とはイテレーションを繰り返してデータセット全体を 1 周することを表します。
はいはい
例えば、 (100, 'epoch') と指定すると、トレーナは 100 エポックで訓練を終了します。
そうだね
(100, 'iteration') と指定すると、100 イテレーション後に訓練を終了します。
はい
トレーナを作るときにこの引数 (stop_trigger) を指定しないと、訓練は自動的には止まらず、永久にループが回り続けることになります。
こわいこわい
ここでは 30 エポック分ループを実行した時点で停止するトレーナオブジェクトを作成します。
code:py
trainer = training.Trainer(updater, (30, 'epoch'), out='results/iris_result1')
1 つ目の引数にループ処理を担当するアップデータオブジェクトを渡し、2 つ目に停止条件を表すタプルを指定します。
updater を渡していて、updater は train_iter (=バッチサイズごとに分割されたデータ)を持っているので、このデータを用いで訓練していくんですなぁ〜
out 引数は、ログや訓練途中のパラメータの値など、次節で解説するエクステンションを用いて行われる訓練ループに加わる付加的な処理の結果を保存する場所を指定します。
指定されたパスにディレクトリがない場合は、自動的に作成されます。
便利だね
16.1.7. エクステンション
エクステンションを使うと、トレーナが統括する訓練ループの途中に付加的な処理を追加することができます。
もし何らかの理由によって訓練が中断された場合でも、途中から再開できるように訓練途中のネットワークのパラメータなどをスナップショット (snapshot) として保存しておく、といったことがよく行われます。
こりゃ便利だ
エクステンションをトレーナに追加するには、 trainer.extend() というメソッドを使います。
簡単だね
code:py
from chainer.training import extensions
trainer.extend(extensions.LogReport(trigger=(1, 'epoch'), log_name='log'))
指定された周期で、損失の値や正解率など、後述するレポータ (reporter) がレポートした値を自動的に集計し、Trainer オブジェクト作成時に out 引数で指定したディレクトリに、log_name 引数に指定されたファイル名でそれらの集計された情報を JSON 形式で保存します。
ほう〜
上記のコード中では (1, 'epoch') となっているため、1 エポックが終わる度に毎回レポートされた値を集計し、ログファイルに記録します。
はい
code:py
trainer.extend(extensions.snapshot(filename='snapshot_epoch-{.updater.epoch}'))
トレーナオブジェクトを指定されたタイミング(デフォルトでは 1 エポックごと)で保存します。
はい
トレーナオブジェクトのスナップショットを保存しておけば、その時点から訓練を再開することが可能になります。
便利
また、スナップショットから訓練済みモデルをとりだして推論だけを行いたい場合にもスナップショットを取っておく必要があります。
なるほどね。例えば 10エポック目のモデルと 20エポック目のモデルでテストデータを用いて推論した結果を比較する、みたいなシーンで必要だね。
filename という引数に保存時のファイル名を指定することができます。
この引数に渡された文字列は、内部で filename.format(trainer) とトレーナオブジェクトを使ってフォーマットされるため、保存時のイテレーション数などの情報をファイル名に使用することができます。
便利だ〜
code:py
trainer.extend(extensions.dump_graph('main/loss'))
指定された Variable オブジェクトからたどることができる計算グラフを Graphviz で描画可能な DOT 形式で保存します。
ふむ。
起点となる Variable は名前で指定することもできます。
この例では、'main/loss' という文字列を指定しています。
これは後述するレポータという機能を用いて、L.Classifier 内でレポートされている損失につけられた名前です。
L.Classifier がいい感じにwrapしてくれているおかげ(せい)でブラックボックスになっている部分ちょっとあるね
code:py
trainer.extend(extensions.Evaluator(valid_iter, net, device=-1), name='val')
検証用データセットのイテレータと、訓練を行うネットワークのオブジェクトを渡しておくことで、訓練中に指定されたタイミングで検証用データセットを用いたネットワークの評価を行います。
は〜、なるほど。
valid_iter どこで登場するかな?と思っていたけど、ここかいな。
↑のサンプルコードでは訓練用と検証用しか用意してないんだなぁ。
code:py
LogReport で集計した値を標準出力に出力します。どの値を出力するかをリストの形で与えます。
引数に与えることができる文字列の一覧が欲しいぜ...
レポータ (reporter)の仕様を把握する必要があるな
code:py
trainer.extend(extensions.PlotReport('fc1/W/grad/mean', x_key='epoch', file_name='mean.png')) 第 1 引数に与えられるリストで指定された値の時間変化をグラフに描画し、出力ディレクトリに file_name 引数に指定されたファイル名で画像として保存します。
ずいぶんとリッチな拡張ですなぁ〜
グラフの作成には Matplotlib が使用されるため、Matplotlib がインストールされている必要があります。
はい
PlotReport エクステンションは、複数個追加することができます。
今回は、3 つの PlotReport を追加しています。
引数の指定はだいたい察したわ
code:py
trainer.extend(extensions.ParameterStatistics(net.predictor.fc1, {'mean': np.mean}, report_grads=True))
指定した Link が持つパラメータの平均・分散・最小値・最大値などの統計値を計算し、レポートします。
ほ〜、便利
パラメータが発散していないかなどをチェックするのに便利です。
まともな訓練になっているかどうか、のチェック方法の一つとして頭の片隅に置いておこう
パラメータの勾配を統計値の計算の対象にしたい場合は、report_grads を True にする必要があります。
はい
16.1.8. その他の代表的な拡張
ここで紹介したエクステンションは、上で紹介した以外にも様々なオプションを持っており、柔軟に組み合わせることができます。
は〜い
16.1.9. 訓練の開始
エクステンションの追加まで完了したため、訓練を開始します。
訓練の開始は、trainer.run() メソッドを呼び出すことで行います。
はい
trainer の out 引数に指定した結果出力のためのディレクトリ results/iris_result1 の中身を確認してみましょう。
lsすればいいね
保存されたログファイルを読み込んで、内容を 10 だけ表示してみます。 ログファイルは JSON 形式で保存されているため、Pandas を使って読み込むと、ノートブック上で見やすく表示することができます。
code:py
import json
import pandas as pd
log = json.load(open('results/iris_result1/log'))
df_result = pd.DataFrame(log)
df_result.tail(10)
損失の変遷を記録したグラフを確認します。
グラフの描画結果は、先程内容を確認した results/iris_result1/ ディレクトリの中に loss.png というファイル名で画像として保存されています。
Jupyter Notebook からは IPython モジュールを使うことで、ディスクに保存されている画像を読み込んで表示することができます。
code:py
from IPython.display import Image
Image('results/iris_result1/loss.png')
https://tutorials.chainer.org/ja/_images/16_Trainer_and_Extension_32_0.png
便利〜
code:py
Image('results/iris_result1/accuracy.png')
https://tutorials.chainer.org/ja/_images/16_Trainer_and_Extension_34_0.png
MLP というネットワークが、どのような構造になっているのかを、視覚的に確認する
そんなことまでできるのか〜
dump_graph エクステンションによって出力された DOT ファイルを、pydot パッケージを使って画像に変換する
なるほどね〜
DOT ファイルは、cg.dot というファイル名で結果ディレクトリに保存されています。
これを読み込んで、pydot を使って画像に変換し、それを表示してみましょう。
code:py
import pydot
file = pydot.graph_from_dot_file('results/iris_result1/cg.dot')
file0.write_png('graph.png') Image('graph.png', width=600, height=600)
https://tutorials.chainer.org/ja/_images/16_Trainer_and_Extension_36_0.png
ここまでで、トレーナの基本的な使い方の解説は終了です。 次節からは、より高度な使い方について説明します。
hai
16.2. レポータで様々な値を記録する
PrintReport エクステンションを使うと、現在のエポック、イテレーション、また損失の値や正解率などを標準出力に表示することができました。
これらの値は特にユーザが明示的に指示しなくともデフォルトで LogReport が集計できるようにレポートされているため、このようなことが可能になっています。
集計したい値を明示的に指定し、LogReport に集計させるようにすることも可能です。
本節では、その方法について説明します。
ほう、なるほど。
ネットワークの中で行われる計算の途中結果などを毎イテレーション集計しておき、値の変化を確認したい場合は、レポータ (reporter) という機能を用います。
計算の途中結果 なるほどね。なんか研究の考察とかで使いそう〜。
レポータは、chainer.reporter モジュールにある report 関数を使って、観測対象としたい変数を指定することで、その値を集計することができる機能です。
hmhm
まずは、レポータの観測対象に MLP 内の計算の途中結果を追加してみます。
今回は、ネットワークの定義自体を修正し、forward メソッドの中で途中結果を chainer.reporter.report() 関数に渡します。
いってみよ〜
code:py
from chainer import reporter
class MLP2(chainer.Chain):
def __init__(self, n_mid_units=100, n_out=3):
super().__init__()
with self.init_scope():
self.fc1 = L.Linear(None, n_mid_units)
self.fc2 = L.Linear(n_mid_units, n_mid_units)
self.fc3 = L.Linear(n_mid_units, n_out)
def forward(self, x):
h = F.relu(self.fc1(x))
h = F.relu(self.fc2(h))
reporter.report({'avg_y': F.average(h), 'var_y':F.cross_covariance(h, h)}, self) # ここね
h = self.fc3(h)
return h
この MLP2 では、2 層目の fc2 の出力値に ReLU を適用したあとの値について平均と分散を計算し、 avg_y と var_y という名前でレポータに登録しています。
こうすると、forward が呼び出される度にこれらの値がレポートされるようになるため、LogReport はその変遷を集計することができます。
なるほどね〜
このネットワークを訓練して、新しくレポートされる値を PrintReport を用いて確認してみましょう。
まずは、新しいネットワークの訓練のための trainer オブジェクトを作成します。
code:py
# ネットワーク (+ Classifier)
net = L.Classifier(MLP2())
# オプティマイザ
optimizer = optimizers.MomentumSGD(lr=0.1).setup(net)
# イテレータ
train_iter = iterators.SerialIterator(train, 32)
# アップデータ
updater = training.StandardUpdater(train_iter, optimizer, device=-1) # device=-1でCPUでの計算実行を指定
# トレーナ
trainer = training.Trainer(updater, (30, 'epoch'))
次に、 LogReport と PrintReport を設定します。
このとき他のエクステンションも LogReport が集計した値を用いるため、LogReport の追加は必須です。
PrintReport には、表示したい値の名前を設定します。
code:py
trainer.extend(extensions.LogReport())
trainer.extend(extensions.PrintReport([
'epoch', 'iteration',
'main/accuracy',
'main/predictor/avg_y',
'main/predictor/var_y',
]))
訓練を開始します。
code:py
trainer.run()
新しくレポートした avg_y、var_y の集計結果が出力されています。
はい
16.3. 訓練の早期終了
早期終了 (early stopping) とは、過学習を避けるために行う正則化の一種で、訓練用データセットにフィットしすぎてしまい、途中からテスト用データセットでのエラーが大きくなっていってしまう前に、訓練を途中で打ち切る方法をいいます。
は〜、なるほどね。
Chainer では EarlyStoppingTrigger オブジェクトを作成し、これを訓練終了タイミングを指示するタプルの代わりにトレーナに渡すことで行えます。
ふむふむ。(30, 'epoch') とかの代わりに渡すのね?
EarlyStoppingTrigger には、どの指標を用いて早期終了の判断を行うかと、最大の訓練の長さなどを指定します。
hai
code:py
net = L.Classifier(MLP())
train_iter = iterators.SerialIterator(train, batchsize)
valid_iter = iterators.SerialIterator(valid, batchsize, False, False)
optimizer = optimizers.MomentumSGD(lr=0.1).setup(net) # 早期終了が発生するよう、学習率を高く設定
updater = training.StandardUpdater(train_iter, optimizer, device=-1)
from chainer.training.triggers import EarlyStoppingTrigger
trigger = EarlyStoppingTrigger(monitor='val/main/loss', check_trigger=(1, 'epoch'),
patients=5, max_trigger=(30, 'epoch'))
trainer = training.Trainer(updater, trigger, out='results/iris_result5')
てか、この節だけじゃなくて、この章ずっと lr=0.1 になってるんだけど、typoかな?
全然いい結果出てないんだよね〜
早期終了は、EarlyStoppingTrigger のインスタンスを Trainer のコンストラクタの stop_trigger 引数に渡すことで設定します。
EarlyStoppingTrigger のコンストラクタに渡す引数で、挙動を定義します。
引数の意味はだいたい察した
patients は、早期終了のしやすさを指定します。たとえば 3 を指定すると、チェック時にそれまでの最良の値を更新できないことが 3 回連続で続いた場合に限って、早期終了するという動作になります。
これだけは察せなかった。なるほどね
早期終了の様子が確認しやすいようにエクステンションを設定します。
また、検証用データセットに対する正解率を monitor に使用するため、Evaluator を使って検証用データセットに対する正解率を毎エポック計算します。
trigger の引数に monitor='val/main/loss'
code:py
from chainer.training import extensions
trainer.extend(extensions.LogReport(trigger=(1, 'epoch'), log_name='log'))
trainer.extend(extensions.Evaluator(valid_iter, net, device=-1), name='val')
trainer.extend(extensions.PrintReport([
'epoch', 'main/loss', 'main/accuracy',
'val/main/loss', 'val/main/accuracy', 'elapsed_time']))
訓練を実行します。
code:py
trainer.run()
code:log
epoch main/loss main/accuracy val/main/loss val/main/accuracy elapsed_time
1 9.14637 0.367188 3.18421 0.266667 0.0365386
2 1.58648 0.333333 0.847107 0.666667 0.470719
3 0.70513 0.739583 0.604198 0.6 0.886571
4 0.520308 0.65625 0.674695 0.6 1.83415
5 0.398812 0.84375 0.309532 0.933333 2.21862
6 0.286818 0.885417 1.18771 0.666667 2.651
7 1.0598 0.6875 0.540349 0.666667 3.03917
8 0.580482 0.65625 0.454071 0.8 3.44127
9 0.522949 0.739583 1.01887 0.466667 3.84291
10 0.61402 0.625 1.03565 0.666667 4.25336
最大エポック数には 30 を指定していましたが、それよりも早く訓練が終了しました。
しました
早期終了を使用すると、特定の指標での改善が見られなくなった時点で訓練を停止させることができるため、効果の薄い計算が続くことを防ぐことにもなり、計算資源の節約にもなります。
わーい、便利。
本章では、前章まで行っていたような訓練ループを明示的に書く方法ではなく、トレーナを使って訓練ループを設定し、エクステンションを使って様々な訓練時の情報を集計したり、可視化したり、活用したりする方法を紹介しました。
はーい、お疲れ様でした!
次章では画像処理の基礎について解説します。
.........次章???
見当たらないけど......
おわった....のか.....???